外部ネットワークアクセスで RDS PostgreSQL に IAM認証で接続してみた #SnowflakeDB

外部ネットワークアクセスで RDS PostgreSQL に IAM認証で接続してみた #SnowflakeDB

Clock Icon2024.11.18

はじめに

2024年11月のリリースで、外部ネットワークアクセスを使用する Snowpark UDF やストアドプロシージャから AWS サービスに対する IAM 認証のサポートが一般提供となりました。
さらに、同年10月のリリースでは、Snowpark から外部ネットワークにアクセスする際に AWS PrivateLink を利用する機能も一般提供されています。

これらの機能を組み合わせることで、プライベートサブネット内に構築した RDS に対して、IAM ロールベースのデータベース認証を使用する接続が可能になります。
IAM データベース認証では一時的な認証情報を使用しますが、この認証情報を取得する API を IAM 認証を設定した Snowflake インスタンスから呼び出す場合も、一時的な認証情報が利用されるため、認証情報の管理がよりセキュアになります。

これらの機能を実際に試してみましたので、本記事でその内容をまとめてみます。

https://docs.snowflake.com/en/release-notes/2024/8_43#authentication-with-aws-iam-from-procedures-and-functions-general-availability

前提条件

本記事では、以下の環境で検証を行いました。
PrivateLink 経由で RDS PostgreSQL に IAM データベース認証を行い UDF から PostgreSQL のデータを参照することを目指します。

  • Snowflake
    • Business Ciritical
      • PrivateLink の設定には Business Ciritical 以上のエディションが必要
    • クラウドリージョン:AWS_AP_NORTHEAST_1
  • AWS
    • Snowflake アカウントと同一のリージョンに VPC を作成
  • Amazon RDS PostgreSQL:16.3-R2
    • プライベートサブネットに構築
    • インスタンスタイプ:db.t3.micro
    • RDS の DB インスタンスで IAM データベース認証を有効化済み

下図の構成です。

image

また、前提として上記の構成は設定済みで、すでに RDS PostgreSQL に対してはデータベースユーザーのユーザー名・パスワード認証にょる接続は可能な状態であるとします。こちらの構成手順については、以下をご参照ください。

https://dev.classmethod.jp/articles/aws-privatelink-snowpark-external-network-access-try-snowflakedb/

サンプルデータ

サンプルデータには PostgreSQL 側で作成済みの以下のデータベース・テーブルを使用します。

-- データベースを作成
CREATE DATABASE sample_db;

\c sample_db;

-- テーブル: customers 
CREATE TABLE customers (
    customer_id SERIAL PRIMARY KEY,  
    name VARCHAR(100),               
    email VARCHAR(100),              
    age INT                          
);

-- customers テーブルにサンプルデータを挿入
INSERT INTO customers (name, email, age) VALUES
('Alice Smith', 'alice@example.com', 30),
('Bob Johnson', 'bob@example.com', 25),
('Charlie Brown', 'charlie@example.com', 35);

AWS側の作業

手順は以下に記載があるので、こちらに沿って進めます。

https://repost.aws/ja/knowledge-center/users-connect-rds-iam
https://dev.classmethod.jp/articles/iam-db-auth-to-postgresql/

AWS 認証トークンを使用するデータベースユーザーアカウントを作成

踏み台サーバから PostgreSQL に接続し、以下のコマンドで IAM データベース認証用のデータベースユーザーを作成します。

--専用のロールを作成
CREATE ROLE readonly_role;
GRANT CONNECT ON DATABASE sample_db TO readonly_role;

\c sample_db;
GRANT USAGE ON SCHEMA public TO readonly_role;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO readonly_role;

-- IAMデータベース認証用のユーザーを作成し権限を付与
CREATE USER iam_ro_user WITH LOGIN;
GRANT rds_iam TO iam_ro_user;
GRANT readonly_role TO iam_ro_user;

https://aws.amazon.com/jp/blogs/news/securing-amazon-rds-and-aurora-postgresql-database-access-with-iam-authentication/

データベースユーザーを IAM ロールにマッピングする IAM ポリシーを追加

以下の内容で IAM ポリシーを作成します。

{
   "Version": "2012-10-17",
   "Statement": [
      {
         "Effect": "Allow",
         "Action": [
             "rds-db:connect"
         ],
         "Resource": [
             "arn:aws:rds-db:ap-northeast-1:<account-id>:dbuser:<dbi-resource-id>/iam_ro_user"
         ]
      }
   ]
}

https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.IAMPolicy.html

Amazon RDS のアクセスを許可する IAM ロールを作成

以下の内容で IAM ロールを作成します。

  • 信頼されたエンティティのタイプ:AWS アカウント
  • オプション
    • 外部 ID を要求する

アカウントIDには一時的に自身のアカウントを指定し、外部 ID にも適当な値を指定しておきます。これらの値は後述する手順で更新します。
この時点では以下のようになります。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "sts:AssumeRole",
            "Principal": {
                "AWS": "<自身のAWSアカウントID>"
            },
            "Condition": {
                "StringEquals": {
                    "sts:ExternalId": "<任意の値>"
                }
            }
        }
    ]
}

Snowflake側の作業

ネットワークルールの作成

はじめにMODE = EGRESS、TYPE = PRIVATE_HOST_PORTとするネットワークルールを作成します。これにより外部サービスへのプライベートエンドポイント経由でのアクセスの許可・制限を行います。
VALUE_LISTには、RDS のエンドポイントとポートを指定します。注意点として、ポートの指定まで含めるようにします。この指定がない場合、デフォルトで 443 となり、サービスや設定よっては接続できなくなってしまいます。

CREATE OR REPLACE NETWORK RULE aws_rds_postgres_network_rule
    MODE = EGRESS
    TYPE = PRIVATE_HOST_PORT
    VALUE_LIST = ('database-1.xxxxx.ap-northeast-1.rds.amazonaws.com:5432');

API 認証用のセキュリティ統合を作成

IAM 認証を使用する外部認証用のセキュリティ統合を作成します。これにより、Snowflake アカウント側でセキュリティ統合が参照する IAM ユーザーが作成されます。AWS_ROLE_ARNには上記の手順で作成した IAM ロールの Arn を指定します。デフォルトでは ACCOUNTADMIN のみ実行可能です。

CREATE OR REPLACE SECURITY INTEGRATION rds_security_integration
  TYPE = API_AUTHENTICATION
  AUTH_TYPE = AWS_IAM
  ENABLED = TRUE
  AWS_ROLE_ARN = 'arn:aws:iam::<account-id>:role/sf-udf-rds-auth-role';

統合オブジェクトを作成後、作成したオブジェクトに対して以下を実行します。

DESC  SECURITY INTEGRATION rds_security_integration;

出力の内、以下の内容を控えておきます。

  • API_AWS_IAM_USER_ARN
    • 統合オブジェクトが参照する Snowflake アカウント側の IAM ユーザー
  • API_AWS_EXTERNAL_ID

AWS側:IAM ロールの信頼関係を更新

IAMロールの信頼関係を以下のように更新します。

{
	"Version": "2012-10-17",
	"Statement": [
		{
			"Effect": "Allow",
			"Principal": {
				"AWS": "<API_AWS_IAM_USER_ARN>"
			},
			"Action": "sts:AssumeRole",
			"Condition": {
				"StringEquals": {
					"sts:ExternalId": "<API_AWS_EXTERNAL_ID>"
				}
			}
		}
	]
}

シークレットの作成

IAM ロールに設定された権限を引き受け、一時的な認証情報を得るためのシークレットオブジェクトを作成します。

CREATE OR REPLACE SECRET aws_rds_access_token
  TYPE = CLOUD_PROVIDER_TOKEN
  API_AUTHENTICATION = rds_security_integration;

https://docs.snowflake.com/en/sql-reference/sql/create-secret#aws-iam-required-parameters

外部ネットワークアクセス統合の作成

さいごに外部アクセス統合として、これまでに作成したネットワークルール・シークレットを紐づけます。デフォルトでは ACCOUNTADMIN のみ実行可能です。

--外部ネットワークアクセスを作成
CREATE OR REPLACE EXTERNAL ACCESS INTEGRATION rds_external_access_integration
  ALLOWED_NETWORK_RULES = (<db>.<schema>.aws_rds_postgres_network_rule)
  ALLOWED_AUTHENTICATION_SECRETS=(<db>.<schema>.aws_rds_access_token)
  ENABLED=true ;

https://docs.snowflake.com/en/sql-reference/sql/create-external-access-integration

PythonUDF の作成

Snowflake UDF からの IAM データベース認証の使用は、以下のステップを行います。

  1. IAM ロールを引き受ける一時的な認証情報を取得(Snowflake 側 IAM ユーザーがロールを引き受け)
  2. 取得した一時的な認証情報で RDS が発行する認証トークンを取得
  3. 取得した認証トークンでデータベースに接続

IAM ロールを引き受ける一時的な認証情報を取得

RDS に接続する前に Snowpark UDF で一時的な認証情報を取得する手順を確認します。ここでは、以下のような Python UDF を作成し、実行してみます。

CREATE OR REPLACE FUNCTION fetch_temp_credentials()
RETURNS STRING
LANGUAGE PYTHON
RUNTIME_VERSION = 3.11
HANDLER = 'get_secret_username_password'
EXTERNAL_ACCESS_INTEGRATIONS = (rds_iam_external_access_integration)
SECRETS = ('cred' = <db>.<schema>.aws_rds_access_token)
AS
$$
import _snowflake
import json

def get_secret_username_password():
  cloud_provider_object = _snowflake.get_cloud_provider_token('cred')
  cloud_provider_dictionary = {
          "ACCESS_KEY_ID": cloud_provider_object.access_key_id,
          "SECRET_ACCESS_KEY": cloud_provider_object.secret_access_key,
          "TOKEN": cloud_provider_object.token
      }
  return json.dumps(cloud_provider_dictionary)
$$;

シークレットの情報から取得された IAM ロール引き比べのための一時的な認証情報の取得には_snowflakeモジュールを使用できます。

  • get_cloud_provider_token(cloud_provider_secret_name)でセッションを作成するための情報を持つオブジェクトが返される
  • 属性として、access_key_idsecret_access_keytokenが含まれる

この場合、出力は以下のようになります。

{
	"ACCESS_KEY_ID": "xxxxxxxxxx", 
	"SECRET_ACCESS_KEY": "xxxxx", 
	"TOKEN": "xxxxxxxxxxxxxxxxxx"
}

RDS に IAM データベース認証で接続

RDS への IAM データベース認証を行うためには、認証トークンを取得する必要があります。このトークンは、上記の手順で取得した一時的な認証情報を使用したセッション内で発行されます。
この工程も含めた UDF として、ここでは以下の内容としました。

CREATE OR REPLACE function query_postgres()
RETURNS VARIANT
LANGUAGE PYTHON
RUNTIME_VERSION = 3.11
IMPORTS=('@my_int_stage/ap-northeast-1-bundle.pem')
PACKAGES = ('psycopg2','boto3')
HANDLER = 'main'
EXTERNAL_ACCESS_INTEGRATIONS = (rds_iam_external_access_integration)
SECRETS = ('cred' = <db>.<schema>.aws_rds_access_token)
AS $$
import psycopg2
import sys
import os
import json
import boto3
import _snowflake

# RDS PostgreSQL の情報を設定
ENDPOINT = "database-1.xxxxx.ap-northeast-1.rds.amazonaws.com"
DBNAME = "sample_db"
USER = "iam_ro_user"
PORT = 5432
REGION = "ap-northeast-1"

# Snowflake UDFのインポートディレクトリから証明書ファイルのパスを取得
IMPORT_DIRECTORY_NAME = "snowflake_import_directory"
import_dir = sys._xoptions[IMPORT_DIRECTORY_NAME]
ssl_cert_path = os.path.join(import_dir, 'ap-northeast-1-bundle.pem')  # 証明書ファイルのパス

def main():
    # シークレットから一時的な認証情報を取得
    cloud_provider_object = _snowflake.get_cloud_provider_token('cred')
    cloud_provider_dictionary = {
          "ACCESS_KEY_ID": cloud_provider_object.access_key_id,
          "SECRET_ACCESS_KEY": cloud_provider_object.secret_access_key,
          "TOKEN": cloud_provider_object.token
      }

    # 認証情報の割り当てとリージョンを指定
    boto3_session_args = {
          'aws_access_key_id': cloud_provider_dictionary["ACCESS_KEY_ID"],
          'aws_secret_access_key': cloud_provider_dictionary["SECRET_ACCESS_KEY"],
          'aws_session_token': cloud_provider_dictionary["TOKEN"],
          'region_name': 'ap-northeast-1'
      }

    # セッションを作成
    session = boto3.Session(**boto3_session_args)

    # RDS を操作するためのクライアントを作成
    client = session.client('rds')

    # RDS が発行する認証トークンを取得
    token = client.generate_db_auth_token(DBHostname=ENDPOINT, Port=PORT, DBUsername=USER, Region=REGION)

    # PostgreSQLに接続
    conn = psycopg2.connect(
        host=ENDPOINT,
        port=PORT,
        database=DBNAME,
        user=USER,
        password=token,
        sslmode='verify-ca',
        sslrootcert=ssl_cert_path # インポートディレクトリから取得した証明書ファイルを指定
    )

    # クエリを実行
    cur = conn.cursor()
    cur.execute("SELECT * FROM customers")
    result = cur.fetchall()

    # 接続を閉じる
    conn.close()
    return result
$$;

こちらは以下を参考とさせていただきました。

https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.Connecting.Python.html

UDF を実行すると下図のようになり、PostgreSQL のデータを参照できました。

image 2

最後に、簡単ではありますが実行する SQL を変えて INSERT を実行する UDF としてみます。

insert 用の UDF
CREATE OR REPLACE function insert_postgres()
RETURNS VARIANT
LANGUAGE PYTHON
RUNTIME_VERSION = 3.11
IMPORTS=('@my_int_stage/ap-northeast-1-bundle.pem')
PACKAGES = ('psycopg2','boto3')
HANDLER = 'main'
EXTERNAL_ACCESS_INTEGRATIONS = (rds_iam_external_access_integration)
SECRETS = ('cred' = <db>.<schema>.aws_rds_access_token)
AS $$
import psycopg2
import sys
import os
import json
import boto3
import _snowflake

# RDS PostgreSQL の情報を設定
ENDPOINT = "database-1.xxxxx.ap-northeast-1.rds.amazonaws.com"
DBNAME = "sample_db"
USER = "iam_ro_user"
PORT = 5432
REGION = "ap-northeast-1"

# Snowflake UDFのインポートディレクトリから証明書ファイルのパスを取得
IMPORT_DIRECTORY_NAME = "snowflake_import_directory"
import_dir = sys._xoptions[IMPORT_DIRECTORY_NAME]
ssl_cert_path = os.path.join(import_dir, 'ap-northeast-1-bundle.pem')  # 証明書ファイルのパス

def main():
    # シークレットから一時的な認証情報を取得
    cloud_provider_object = _snowflake.get_cloud_provider_token('cred')
    cloud_provider_dictionary = {
          "ACCESS_KEY_ID": cloud_provider_object.access_key_id,
          "SECRET_ACCESS_KEY": cloud_provider_object.secret_access_key,
          "TOKEN": cloud_provider_object.token
      }

    # 認証情報の割り当てとリージョンを指定
    boto3_session_args = {
          'aws_access_key_id': cloud_provider_dictionary["ACCESS_KEY_ID"],
          'aws_secret_access_key': cloud_provider_dictionary["SECRET_ACCESS_KEY"],
          'aws_session_token': cloud_provider_dictionary["TOKEN"],
          'region_name': 'ap-northeast-1'
      }

    # セッションを作成
    session = boto3.Session(**boto3_session_args)

    # RDS を操作するためのクライアントを作成
    client = session.client('rds')

    # RDS が発行する認証トークンを取得
    token = client.generate_db_auth_token(DBHostname=ENDPOINT, Port=PORT, DBUsername=USER, Region=REGION)

    # PostgreSQLに接続
    conn = psycopg2.connect(
        host=ENDPOINT,
        port=PORT,
        database=DBNAME,
        user=USER,
        password=token,
        sslmode='verify-ca',
        sslrootcert=ssl_cert_path # インポートディレクトリから取得した証明書ファイルを指定
    )

    # クエリを実行
    cur = conn.cursor()
    cur.execute(
    "INSERT INTO customers (name, email, age) VALUES (%s, %s, %s)",
    ('Alice Johnson', 'alice.johnson@example.com', 28)
    )
    result = cur.fetchall()

    # 接続を閉じる
    conn.close()
    return result
$$;

IAM データベース認証用のユーザーは、読み取り専用権限しか与えていないので、書き込みを処理を行う UDF を実行すると下図のエラーとなります。

image 3

さいごに

外部ネットワークアクセスを使用する Snowpark UDF から RDS に IAM データベース認証で接続してみました。
IAM データベース認証では一時的な認証情報(トークン)を使用しますし、Snowflake との連携時も IAM 認証を使用することで、Snowflake 側でもトークン呼び出しに一時的な認証情報が利用されるため、よりセキュアになります。
こちらの内容が何かの参考になれば幸いです。

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.